/**
 * 	Copyright 2011 rkehoe
 *
 * 	Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 * 	You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *	limitations under the License.
 *
 */
package queryable.store.sql;

import java.rmi.server.UID;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import javax.sql.DataSource;

import org.springframework.jdbc.core.RowMapper;

import queryable.store.EntityReflector;
import queryable.store.EntityReflector.DomainObjectMetadata;
import queryable.store.AbstractStore;
import queryable.store.IDGenerator;
import queryable.store.IFieldMetadata;
import queryable.store.IStore;
import queryable.store.annotations.Index;

/**
 * Simple JDBC store.
 * 
 * @author rkehoe
 * @param <T>
 * 
 */
public class JDBCStore<T> extends AbstractStore<T>
{
	private static final int	           BATCT_INSERT_SIZE	= 1000;
	private EntityReflector	               entityReflector;
	private PreparedStatement	           insertPstmt;
	private Map<String, PreparedStatement>	pStmtCache;
	private final ISQLGenerator	           sqlGenerator;
	private String	                       tableName;
	private final DataSource	           dataSource;
	private Connection	                   preparedStatementConnection;
	private final IDGenerator idGenerator;
	
	public JDBCStore(String name, Class<T> domainObjectClass, IStorageProvider storageProvider) throws Exception
	{
		this.entityReflector = new EntityReflector();
		DomainObjectMetadata entityMetadata = this.entityReflector.getDomainObjectMetadata(domainObjectClass);

		this.tableName = name;
		if (name == null || name.isEmpty())
			if (entityMetadata.getTable() != null)
			{
				String storeName = entityMetadata.getTable().name();
				this.tableName = storeName;
			}
			else
			{
				String storeName = domainObjectClass.getSimpleName();
				this.tableName = storeName;
			}

		this.dataSource = storageProvider.getDataSource();
		this.preparedStatementConnection = this.dataSource.getConnection();
		this.preparedStatementConnection.setAutoCommit(true);

		List<IFieldMetadata> fields = entityMetadata.getQueryFields();
		List<Index> indexes = entityMetadata.getIndexes();

		IFieldMetadata blobField = new IFieldMetadata()
		{

			@Override
			public String getName()
			{
				return "object";
			}

			@Override
			public Class<? extends Object> getType()
			{
				return Object.class;
			}

		};

		fields.add(blobField);

		this.sqlGenerator = storageProvider.getGenerator();

		String tableDDL = sqlGenerator.getTableDDL(fields, tableName);
		System.out.println("Creating Objects: " + tableDDL);

		Statement ddlStatement = preparedStatementConnection.createStatement();
		ddlStatement.execute(tableDDL);

		for (Index index : indexes)
		{
			List<String> indexDDL = sqlGenerator.getIndexDDL(tableName, null, index.columnNames());
			for (String indxStmt : indexDDL)
			{
				System.out.println("Creating indx: " + indxStmt);
				ddlStatement.execute(indxStmt);
			}
		}

		ddlStatement.close();

		String insertDML = sqlGenerator.getInsertDML(fields, tableName);
		System.out.println("Generating Insert Statement: " + insertDML);
		this.insertPstmt = preparedStatementConnection.prepareStatement(insertDML);
		this.pStmtCache = new HashMap<String, PreparedStatement>();
		
		this.idGenerator = createIDGenerator(entityMetadata);
	}

	/**
	 * Create an ID generator based on the Type of
	 * the @Id field - currently only longs and strings
	 * are handled.
	 * 
     * @param metadata 
	 * @return
     */
    private IDGenerator createIDGenerator(DomainObjectMetadata md)
    {
    	Class<? extends Object> idType = md.getIdFieldMetadata().getType();
    	if(idType == Long.class)
    	return new SimpleLongIncrementor();
    	else return new UIDGenerator(); 
    			
    }


	private Object generateUID()
	{
		return this.idGenerator.nextID();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see queryable.store.IDAO#insert(T)
	 */
	@Override
	public void add(T entity)
	{
		this.writeLock.lock();
		try
		{
			entityReflector.setIDifNull(this.idGenerator, entity);
			Map<String, Object> properties = entityReflector.getProperties(entity);
			int ii = 1;
			for (Entry<String, Object> property : properties.entrySet())
			{
				Object value = property.getValue();
				insertPstmt.setObject(ii, value);
				ii++;
			}
			insertPstmt.setObject(ii, entity);
			insertPstmt.executeUpdate();
		}
		catch (Exception e)
		{
			throw new RuntimeException(e);
		}
		finally
		{
			this.writeLock.unlock();
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see queryable.store.IDAO#insert(java.util.Collection)
	 */
	@Override
	public void addAll(Collection<T> list)
	{
		this.writeLock.lock();
		try
		{
			insertPstmt.clearBatch();
			int jj = 1;
			for (T entity : list)
			{
				entityReflector.setIDifNull(this.idGenerator, entity);
				Map<String, Object> properties = entityReflector.getProperties(entity);

				int ii = 1;
				for (Entry<String, Object> entry : properties.entrySet())
				{
					Object value = entry.getValue();
					insertPstmt.setObject(ii, value);
					ii++;
				}
				insertPstmt.setObject(ii, entity);
				insertPstmt.addBatch();

				jj++;

				if (jj % BATCT_INSERT_SIZE == 0)
					insertPstmt.executeBatch();
			}
			insertPstmt.executeBatch();
		}
		catch (Exception e)
		{
			throw new RuntimeException(e);
		}
		finally
		{
			this.writeLock.unlock();
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see queryable.store.IStore#clear()
	 */
	@Override
	public void clear()
	{
		this.writeLock.lock();
		try
		{
			this.executeUpdate("truncate table " + this.tableName, new Object[0]);
		}
		catch (Exception e)
		{
			throw new RuntimeException(e);
		}
		finally
		{
			this.writeLock.unlock();
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see queryable.store.IStore#remove(java.lang.String, java.lang.Object[])
	 */
	@Override
	public int remove(String predicate, Object[] args)
	{
		this.writeLock.lock();
		try
		{
			String query = "delete from " + this.tableName + " where " + predicate;
			int rowCount = this.executeUpdate(query, args);
			return rowCount;
		}
		catch (Exception e)
		{
			throw new RuntimeException(e);
		}
		finally
		{
			this.writeLock.unlock();
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see queryable.store.IDAO#size()
	 */
	@Override
	public int size()
	{
		RowMapper<Integer> rowMapper = new RowMapper<Integer>()
		{
			@Override
			public Integer mapRow(ResultSet rs, int arg1) throws SQLException
			{
				return rs.getInt(1);
			}
		};
		List<Integer> select = this.select("count(*)", null, null, rowMapper);
		return select.get(0);
	}

	private int executeUpdate(String statement, Object[] args) throws Exception
	{
		if(args==null)args = new Object[0];
		PreparedStatement pstmt = this.pStmtCache.get(statement);
		if (pstmt == null)
		{
			pstmt = this.preparedStatementConnection.prepareStatement(statement);
			this.pStmtCache.put(statement, pstmt);
			System.out.println("Creating PreparedStatement: " + pstmt);
		}
		pstmt.clearBatch();
		for (int ii = 0; ii < args.length; ii++)
		{
			pstmt.setObject(ii + 1, args[ii]);
		}
		return pstmt.executeUpdate();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see queryable.store.IDAO#select(java.lang.String, java.lang.String,
	 * java.lang.Object[], org.springframework.jdbc.core.RowMapper)
	 */
	@Override
	public <R> List<R> select(String fields, String predicate, Object[] args, RowMapper<R> rowMapper)
	{
		String query = "select " + fields + " from " + this.tableName;
		if (predicate != null && !predicate.isEmpty())
			query = query + " where " + predicate;
		try
		{
			List<R> list = new ArrayList<R>();
			ResultSet resultSet = this.execute(query, args);
			int ii = 0;
			while (resultSet.next())
			{
				R mapRow = rowMapper.mapRow(resultSet, ++ii);
				list.add(mapRow);
			}
			return list;
		}
		catch (Exception e)
		{
			throw new RuntimeException(e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see queryable.store.IDAO#selectWhere(java.lang.String,
	 * java.lang.Object[])
	 */
	@Override
	public List<T> select(String predicate, Object[] args)
	{
		String query = "select object from " + this.tableName + " where " + predicate;
		try
		{
			ResultSet resultSet = this.execute(query, args);
			return this.toCollection(resultSet);
		}
		catch (Exception e)
		{
			throw new RuntimeException(e);
		}
	}

	private ResultSet execute(String statement, Object[] args) throws Exception
	{
		if (args == null)
			args = new Object[0];
		PreparedStatement pstmt = this.pStmtCache.get(statement);
		if (pstmt == null)
		{
			pstmt = this.preparedStatementConnection.prepareStatement(statement);
			this.pStmtCache.put(statement, pstmt);
			System.out.println("Creating PreparedStatement: " + pstmt);
		}
		pstmt.clearBatch();
		for (int ii = 0; ii < args.length; ii++)
		{
			pstmt.setObject(ii + 1, args[ii]);
		}
		return pstmt.executeQuery();
	}

	@SuppressWarnings("unchecked")
	private final List<T> toCollection(ResultSet rs) throws Exception
	{
		ArrayList<T> list = new ArrayList<T>();
		while (rs.next())
		{
			list.add((T) rs.getObject(1));
		}
		return list;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see queryable.store.IStore#isEmpty()
	 */
	@Override
	public boolean isEmpty()
	{
		return this.size() == 0;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see queryable.store.IStore#iterator()
	 */
	@Override
	public Iterator<T> iterator()
	{
		try
		{
			final ResultSet resultSet = execute("select object from " + tableName, new Object[0]);
			Iterator<T> it = new Iterator<T>()
			{
				@Override
				public boolean hasNext()
				{
					try
					{
						return resultSet.next();
					}
					catch (SQLException e)
					{
						throw new RuntimeException(e);
					}
				}

				@SuppressWarnings("unchecked")
				@Override
				public T next()
				{
					try
					{
						return (T) resultSet.getObject(1);
					}
					catch (SQLException e)
					{
						throw new RuntimeException(e);
					}
				}

				@Override
				public void remove()
				{
					// Optional operation/Not supported.
				}
			};
			return it;
		}
		catch (Exception e)
		{
			throw new RuntimeException(e);
		}
	}

}
